<?php
/**
 * This file supplies a memcached store backend for OpenID servers and
 * consumers.
 *
 * PHP versions 4 and 5
 *
 * LICENSE: See the COPYING file included in this distribution.
 *
 * @package OpenID
 * @author Artemy Tregubenko <me@arty.name>
 * @copyright 2008 JanRain, Inc.
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache
 * Contributed by Open Web Technologies <http://openwebtech.ru/>
 */

/**
 * Import the interface for creating a new store class.
 */
require_once 'Auth/OpenID/Interface.php';

/**
 * This is a memcached-based store for OpenID associations and
 * nonces. 
 * 
 * As memcache has limit of 250 chars for key length, 
 * server_url, handle and salt are hashed with sha1(). 
 *
 * Most of the methods of this class are implementation details.
 * People wishing to just use this store need only pay attention to
 * the constructor.
 *
 * @package OpenID
 */
class Auth_OpenID_MemcachedStore extends Auth_OpenID_OpenIDStore {

  /**
   * Initializes a new {@link Auth_OpenID_MemcachedStore} instance.
   * Just saves memcached object as property.
   *
   * @param resource connection Memcache connection resourse
   */
  function Auth_OpenID_MemcachedStore($connection, $compress = false) {
    $this->connection = $connection;
    $this->compress = $compress ? MEMCACHE_COMPRESSED : 0;
  }

  /**
   * Store association until its expiration time in memcached. 
   * Overwrites any existing association with same server_url and 
   * handle. Handles list of associations for every server. 
   */
  function storeAssociation($server_url, $association) {
    // create memcached keys for association itself 
    // and list of associations for this server
    $associationKey = $this->associationKey($server_url, $association->handle);
    $serverKey = $this->associationServerKey($server_url);
    
    // get list of associations 
    $serverAssociations = $this->connection->get($serverKey);
    
    // if no such list, initialize it with empty array
    if (! $serverAssociations) {
      $serverAssociations = array();
    }
    // and store given association key in it
    $serverAssociations[$association->issued] = $associationKey;
    
    // save associations' keys list 
    $this->connection->set($serverKey, $serverAssociations, $this->compress);
    // save association itself
    $this->connection->set($associationKey, $association, $this->compress, $association->issued + $association->lifetime);
  }

  /**
   * Read association from memcached. If no handle given 
   * and multiple associations found, returns latest issued
   */
  function getAssociation($server_url, $handle = null) {
    // simple case: handle given
    if ($handle !== null) {
      // get association, return null if failed
      $association = $this->connection->get($this->associationKey($server_url, $handle));
      return $association ? $association : null;
    }
    
    // no handle given, working with list
    // create key for list of associations
    $serverKey = $this->associationServerKey($server_url);
    
    // get list of associations
    $serverAssociations = $this->connection->get($serverKey);
    // return null if failed or got empty list
    if (! $serverAssociations) {
      return null;
    }
    
    // get key of most recently issued association
    $keys = array_keys($serverAssociations);
    sort($keys);
    $lastKey = $serverAssociations[array_pop($keys)];
    
    // get association, return null if failed
    $association = $this->connection->get($lastKey);
    return $association ? $association : null;
  }

  /**
   * Immediately delete association from memcache.
   */
  function removeAssociation($server_url, $handle) {
    // create memcached keys for association itself 
    // and list of associations for this server
    $serverKey = $this->associationServerKey($server_url);
    $associationKey = $this->associationKey($server_url, $handle);
    
    // get list of associations
    $serverAssociations = $this->connection->get($serverKey);
    // return null if failed or got empty list
    if (! $serverAssociations) {
      return false;
    }
    
    // ensure that given association key exists in list
    $serverAssociations = array_flip($serverAssociations);
    if (! array_key_exists($associationKey, $serverAssociations)) {
      return false;
    }
    
    // remove given association key from list
    unset($serverAssociations[$associationKey]);
    $serverAssociations = array_flip($serverAssociations);
    
    // save updated list
    $this->connection->set($serverKey, $serverAssociations, $this->compress);
    
    // delete association 
    return $this->connection->delete($associationKey);
  }

  /**
   * Create nonce for server and salt, expiring after 
   * $Auth_OpenID_SKEW seconds.
   */
  function useNonce($server_url, $timestamp, $salt) {
    global $Auth_OpenID_SKEW;
    
    // save one request to memcache when nonce obviously expired 
    if (abs($timestamp - time()) > $Auth_OpenID_SKEW) {
      return false;
    }
    
    // returns false when nonce already exists
    // otherwise adds nonce
    return $this->connection->add('openid_nonce_' . sha1($server_url) . '_' . sha1($salt), 1, // any value here 
$this->compress, $Auth_OpenID_SKEW);
  }

  /**
   * Memcache key is prefixed with 'openid_association_' string. 
   */
  function associationKey($server_url, $handle = null) {
    return 'openid_association_' . sha1($server_url) . '_' . sha1($handle);
  }

  /**
   * Memcache key is prefixed with 'openid_association_' string. 
   */
  function associationServerKey($server_url) {
    return 'openid_association_server_' . sha1($server_url);
  }

  /**
   * Report that this storage doesn't support cleanup
   */
  function supportsCleanup() {
    return false;
  }
}
